﻿//
// File:    Mp3LibraryModel.cs
// Project: MP3 Art Embedder v0.2.x
//

using System;
using System.Collections.Generic;
using System.IO;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using Id3;
using Mp3LibraryViewed;

namespace Mp3Library
{
    public sealed class Mp3LibraryModel : Mp3LibraryModelBase, IDisposable
    {
        private bool cancelPending = false;
        private BackgroundWorker crawler;
        private EncoderParameters encParms;


        public Mp3LibraryModel ()
        {
            // Parameterize jpeg quality (0-100).
            encParms = new EncoderParameters (1);
            encParms.Param[0] = new EncoderParameter (Encoder.Quality, PicQuality);

            crawler = new BackgroundWorker();
            crawler.WorkerSupportsCancellation = true;
            crawler.DoWork += new DoWorkEventHandler (Job);
            crawler.RunWorkerCompleted += WorkComplete;
        }


        public void Dispose ()
        {
            crawler.Dispose();
            encParms.Dispose();
            GC.SuppressFinalize (this);
        }


        public new bool IsVerbose
        {
            get { return isVerbose; }
            set
            {
                isVerbose = value;
                OnIsVerboseChanged();
            }
        }


        public void AddArtFileNames (string separatedFileNames)
        {
            var fileNames = separatedFileNames.Split (new char[] { Path.PathSeparator },
                                                      StringSplitOptions.RemoveEmptyEntries);
            foreach (var fileName in fileNames)
                artFileNames.Add (fileName.Trim());
        }


        public void ClearArtFileNames ()
        {
            artFileNames.Clear();
        }


        public void ShowHelp ()
        {
            foreach (var line in helpText)
                OnActionLogged (line, Strength.Sticky);
        }


        private void Job (object sender, DoWorkEventArgs args)
        {
            UpdateCovers();
        }


        public void Cancel ()
        {
            cancelPending = true;
            crawler.CancelAsync();
        }


        private void WorkComplete (object sender, RunWorkerCompletedEventArgs args)
        {
            // Unhandled exceptions will be passed thru args.Error only if
            // run *outside* the Visual Studio debugger.

            if (cancelPending)
                OnActionLogged ("Operation cancelled", Strength.Sticky);

            if (args.Error != null)
                OnActionLogged ("ERROR: " + args.Error.Message, Strength.Sticky);

            OnActionLogged (GetTotals(), Strength.Sticky);
            OnRunningChanged (false);
        }


        public void UpdateCoversAsync (UpdateMode mode, string directoryName, int maxPicSize)
        {
            this.Mode = mode;
            this.RootName = directoryName;
            this.MaxPicSize = maxPicSize;

            if ((mode & UpdateMode.WhatIf) == 0)
            {
                bool isGo = view.Question
                (
                    "Really update MP3 files with artwork?",
                    "UPDATE WARNING"
                );

                if (! isGo)
                {
                    OnActionLogged ("Operation cancelled.", Strength.Normal);
                    return;
                }
            }

            OnRunningChanged (true);
            crawler.RunWorkerAsync();
        }


        public void UpdateCovers (UpdateMode mode, string directoryName, int maxPicSize)
        {
            this.Mode = mode;
            this.RootName = directoryName;
            this.MaxPicSize = maxPicSize;

            UpdateCovers();
        }


        private void UpdateCovers ()
        {
            CountSkips = 0;
            CountAdds = 0;
            CountReplaces = 0;
            cancelPending = false;

            foreach (var dirNode in GetDirs (MaxPicSize, GetEncoder (ImageFormat.Jpeg), encParms))
            {
                if (dirNode.errorMessage != null)
                {
                    ++CountErrors;
                    LogException ((Mode & UpdateMode.WhatIf) != 0, dirNode.folderFileName, dirNode.errorMessage);
                    continue;
                }

                var dInfo = new DirectoryInfo (dirNode.Current);
                var fileNames = dInfo.GetFiles ("*.mp3");

                if (dirNode.picStream != null)
                {
                    foreach (FileInfo fileInfo in fileNames)
                    {
                        if (cancelPending)
                            return;

                        FileAction action;
                        if (fileInfo.IsReadOnly)
                        {
                            ++CountSkips;
                            action = FileAction.IgnoredReadonly;
                        }
                        else
                            action = WriteMp3Cover (Mode, fileInfo.FullName, dirNode.picStream);

                        LogAction (action, (Mode & UpdateMode.WhatIf) != 0, fileInfo.FullName, dirNode.folderFileName);
                    }
                }
            }
        }


        private FileAction WriteMp3Cover (UpdateMode mode, string fileName, MemoryStream picStream)
        {
            FileAction action = 0;
            var perms = (mode & UpdateMode.WhatIf) != 0? Mp3Permissions.Read : Mp3Permissions.ReadWrite;
            using (var mp3 = new Mp3File (fileName, perms))
            {
                Id3Tag tag;

                try
                {
                    tag = mp3.GetTag (Id3TagFamily.Version2x);
                }
                catch
                {
                    ++CountSkips;
                    return FileAction.IgnoredCantOpen;
                }

                // TODO change this when tagging library supports additional tag versions
                if (tag == null || tag.MajorVersion != 2 || tag.MinorVersion != 3)
                {
                    ++CountSkips;
                    return FileAction.IgnoredNo23Tag;
                }

                // TODO remove this when tagging library supports Unicode encoding
                if (tag.Album != null && tag.Album.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Artists != null && tag.Artists.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Band != null && tag.Band.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.BeatsPerMinute != null && tag.BeatsPerMinute.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Conductor != null && tag.Conductor.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Genre != null && tag.Genre.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Publisher != null && tag.Publisher.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Track != null && tag.Track.EncodingType == Id3.Id3TextEncoding.Unicode
                    || tag.Year != null && tag.Year.EncodingType == Id3.Id3TextEncoding.Unicode)
                    {
                        ++CountSkips;
                        return FileAction.IgnoredNo23Tag;
                    }

                if (tag.Pictures.Count == 0)
                {
                    action |= FileAction.Added;
                    ++CountAdds;
                }
                else
                {
                    if ((mode & UpdateMode.ReplaceExisting) == 0)
                    {
                        ++CountSkips;
                        return action;
                    }

                    tag.Pictures.Clear();
                    action |= FileAction.Replaced;
                    ++CountReplaces;
                }

                var picFrame = new Id3.Frames.PictureFrame();
                picFrame.LoadImage (picStream);
                tag.Pictures.Add (picFrame);

                if ((mode & UpdateMode.WhatIf) == 0)
                    mp3.WriteTag (tag, WriteConflictAction.Replace);
            }
            return action;
        }


        private string GetArtFileName (DirNode node)
        {
            foreach (string fileName in artFileNames)
            {
                var fullName = node.Dirs[node.Index];

                if (fullName.Length > 0 && fullName[fullName.Length-1] != Path.DirectorySeparatorChar)
                    fullName += Path.DirectorySeparatorChar;
                
                fullName += fileName;

                if (File.Exists (fullName))
                    return fullName;
            }
            return null;
        }


        private IEnumerable<DirNode> GetDirs (int scale, ImageCodecInfo codec, EncoderParameters parms)
        {
            var stack = new Stack<DirNode>();

            if (!Directory.Exists (RootName))
                throw new InvalidOperationException ("No such directory");

            for (var node = new DirNode (RootName);;)
            {
                node.errorMessage = null;
                node.picStream = null;
                node.folderFileName = GetArtFileName (node);

                if (node.folderFileName == null)
                {
                    // No artwork here so use the artwork from nearest ancestor.
                    if (stack.Count > 0)
                    {
                        node.folderFileName = stack.Peek().folderFileName;
                        node.picStream = stack.Peek().picStream;
                    }
                }
                else
                {
                    try
                    {
                        using (Image folderPic = Image.FromFile (node.folderFileName))
                        {
                            if (folderPic.Width <= scale && folderPic.Height <= scale)
                            {
                                node.picStream = new MemoryStream();
                                folderPic.Save (node.picStream, ImageFormat.Jpeg);
                            }
                            else
                            {
                                // Proportion new dimensions.
                                int xSize, ySize;
                                if (folderPic.Width <= folderPic.Height)
                                {
                                    xSize = (int) (scale * ((float) folderPic.Width / (float) folderPic.Height) + 0.5);
                                    ySize = scale;
                                }
                                else
                                {
                                    xSize = scale;
                                    ySize = (int) (scale * ((float) folderPic.Height / (float) folderPic.Width) + 0.5);
                                }

                                // Resize the folder image.
                                using (Image resizedPic = new Bitmap (folderPic, new Size (xSize, ySize)))
                                {
                                    node.picStream = new MemoryStream();
                                    resizedPic.Save (node.picStream, codec, parms);
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        node.errorMessage = ex.Message;
                    }
                }

                yield return node;

                string[] dirs = Directory.GetDirectories (node.Dirs[node.Index]);
                if (dirs.Length > 0)
                {
                    stack.Push (node);
                    node = new DirNode (dirs);
                    continue;
                }

                for (;;)
                {
                    if (++node.Index < node.Dirs.Length)
                        break;
                    if (stack.Count == 0)
                        yield break;
                    node = stack.Pop();
                }
            }
        }


        private void LogAction (FileAction action, bool isWhatif, string mp3FileName, string artFullName)
        {
            var strength = Strength.Normal;
            string message = isWhatif? "[What-if] " : "";

            if ((action & FileAction.IgnoredReadonly) != 0)
                message += "Ignored read-only file" + ": " + mp3FileName;
            else if ((action & FileAction.IgnoredNo23Tag) != 0)
            {
                message += "Ignored file missing v2.3/8859-1 tag" + ": " + mp3FileName;
                strength = Strength.Sticky;
            }
            else if ((action & FileAction.IgnoredCantOpen) != 0)
            {
                message += "Ignored unreadable file" + ": " + mp3FileName;
                strength = Strength.Sticky;
            }
            else if ((action & (FileAction.Added | FileAction.Replaced)) == 0)
                message += "Skipped file with existing cover" + ": " + mp3FileName;
            else
            {
                message += (action & FileAction.Replaced) != 0? "Replaced art in " : "Added art to ";
                message += mp3FileName;
                message += (action & FileAction.Replaced) != 0? " with " : " from ";

                for (var fi = Path.GetDirectoryName (artFullName).Length+1; fi < mp3FileName.Length; ++fi)
                    if (mp3FileName[fi] == Path.DirectorySeparatorChar)
                        message += ".." + Path.DirectorySeparatorChar;

                message += Path.GetFileName (Path.GetFileName (artFullName));
            }

            OnActionLogged (message, strength);
        }


        private void LogException (bool isWhatif, string artFullName, string errorMessage)
        {
            string message = (isWhatif? "*What-if* " : "") + "** ERROR: " + errorMessage + " ** " + artFullName;
            OnActionLogged (message, Strength.Sticky);
        }


        static private ImageCodecInfo GetEncoder (ImageFormat format)
        {
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();

            foreach (ImageCodecInfo codec in codecs)
                if (codec.FormatID == format.Guid)
                    return codec;

            return null;
        }
    }
}
